package org.mic.swagger;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.util.UrlPathHelper;
import springfox.documentation.RequestHandler;
import springfox.documentation.spi.service.RequestHandlerProvider;
import springfox.documentation.spring.web.WebMvcRequestHandler;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.*;

@ConfigurationProperties(prefix = "swagger")
public class FeignRequestHandlerProvider implements RequestHandlerProvider, ApplicationContextAware, InitializingBean {

    private final List<RequestHandler> handlerMappings;

    private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();

    private ApplicationContext context;

    private List<String> extpackages = new ArrayList<>();

    public List<String> getExtpackages() {
        return extpackages;
    }

    @Autowired
    public FeignRequestHandlerProvider() {
        handlerMappings = new ArrayList();
    }

    //fetch all interfaces that annotated with @FeignClint
    protected void initHandlerMethods() {
        Set<Class> feignClients = scan();
        feignClients.forEach((clazz) -> {
            detectHandlerMethods(clazz);
        });
    }

    //step 1:fetch all @FeignScan annotations from context'beans
    //step 2:fetch all interfaces that annotated with @FeignClient
    public Set<Class> scan() {
        String DEFAULT_RESOURCE_PATTERN = "**/*.class";
        Set<Class> feignClients = new LinkedHashSet();
        //Map beanMap = context.getBeansWithAnnotation(FeignScan.class);
        //beanMap.values().forEach((bean) -> {
        //FeignScan scan = AnnotationUtils.findAnnotation(bean.getClass(), FeignScan.class);
        //String[] packages = scan.value();

        ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(null);
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
        extpackages.forEach((pack) -> {
            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(pack.trim()) + '/' + DEFAULT_RESOURCE_PATTERN;
            Resource[] resources = null;
            try {
                resources = resolver.getResources(packageSearchPath);
                Arrays.asList(resources).forEach((resource) -> {
                    try {
                        MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
                        Class clazz = Class.forName(metadataReader.getClassMetadata().getClassName());
                        Annotation feignClient = clazz.getDeclaredAnnotation(FeignClient.class);
                        if (feignClient != null) {
                            feignClients.add(clazz);
                        }
                    } catch (ClassNotFoundException | IOException e) {
                    }
                });
            } catch (IOException e) {
            }
        });
        //});
        return feignClients;
    }

    //fetch and register all methods that annotated with @RequestMapping
    private void detectHandlerMethods(final Class<?> handlerType) {
        MethodIntrospector.selectMethods(handlerType, new ReflectionUtils.MethodFilter() {
            public boolean matches(Method method) {
                RequestMappingInfo requestMapping = getMappingForMethod(method, handlerType);
                if (requestMapping != null) {
                    HandlerMethod handlerMethod = createHandlerMethod(handlerType, method);
                    handlerMappings.add(new WebMvcRequestHandler(requestMapping, handlerMethod));
                }
                return requestMapping != null;
            }
        });
    }

    //construct RequestMappingInfo from handlerType' method that annotated with @RequestMapping
    private RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
        RequestMappingInfo info = null;
        RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
        if (methodAnnotation != null) {
            info = createRequestMappingInfo(methodAnnotation);
            RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
            if (typeAnnotation != null) {
                info = createRequestMappingInfo(typeAnnotation).combine(info);
            }
        }
        return info;
    }

    //create RequestMappingInfo from RequestMapping
    protected RequestMappingInfo createRequestMappingInfo(
            RequestMapping requestMapping) {
        this.config.setUrlPathHelper(new UrlPathHelper());
        this.config.setPathMatcher(new AntPathMatcher());
        this.config.setSuffixPatternMatch(true);
        this.config.setTrailingSlashMatch(true);
        this.config.setRegisteredSuffixPatternMatch(false);
        this.config.setContentNegotiationManager(new ContentNegotiationManager());

        return RequestMappingInfo
                .paths(requestMapping.path())
                .methods(requestMapping.method())
                .params(requestMapping.params())
                .headers(requestMapping.headers())
                .consumes(requestMapping.consumes())
                .produces(requestMapping.produces())
                .mappingName(requestMapping.name())
                .options(this.config)
                .build();
    }

    protected HandlerMethod createHandlerMethod(Class<?> handler, Method method) {
        return new FeignHandlerMethod(handler, method);
    }

    public List<RequestHandler> requestHandlers() {
        return this.handlerMappings;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        initHandlerMethods();
    }

    private static class FeignHandlerMethod extends HandlerMethod {

        private final Method method;

        private final Class<?> beanType;

        public FeignHandlerMethod(Class<?> beanType, Method method) {
            //Use Object cheating super Constructor, swagger only use method and beanType properties
            super(new Object(), method);
            this.method = method;
            this.beanType = beanType;
        }

        @Override
        public Method getMethod() {
            return method;
        }

        @Override
        public Class<?> getBeanType() {
            return beanType;
        }
    }
}
